<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>Canvas 思维导图</title>
  <style>
    body { margin: 0; overflow: hidden; }
    canvas { background: #f0f0f0; display: block; cursor: grab; }
    #editor {
      position: absolute;
      display: none;
      font-size: 16px;
      padding: 4px;
      box-sizing: border-box;
    }
    #contextMenu {
      position: absolute;
      display: none;
      background: #fff;
      border: 1px solid #ccc;
      box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
      z-index: 1000;
    }
    #contextMenu div {
      padding: 6px 12px;
      cursor: pointer;
    }
    #contextMenu div:hover {
      background-color: #f0f0f0;
    }
  </style>
</head>
<body>
<canvas id="mindmap" width="1200" height="800"></canvas>
<textarea id="editor"></textarea>
<div id="contextMenu">
  <div id="addChild">添加子节点</div>
  <div id="addSibling">添加兄弟节点</div>
  <div id="deleteNode">删除节点</div>
</div>

<script>
  const canvas = document.getElementById('mindmap');
  const ctx = canvas.getContext('2d');
  const editor = document.getElementById('editor');
  const contextMenu = document.getElementById('contextMenu');
  const addChildBtn = document.getElementById('addChild');
  const addSiblingBtn = document.getElementById('addSibling');
  const deleteBtn = document.getElementById('deleteNode');

  let currentNode = null;
  let offsetX = 0;
  let offsetY = 0;
  let isDraggingCanvas = false;
  let lastMouseX = 0;
  let lastMouseY = 0;

  const MIN_NODE_WIDTH = 120;
  const PADDING = 10;
  const LINE_HEIGHT = 18;
  const MIN_HORIZONTAL_GAP = 140;
  const VERTICAL_GAP = 60;
  const HORIZONTAL_GAP_RATIO = 0.5; // 水平间距与节点宽度的比例
  let nodeIdCounter = 100;

  const rootNode = {
    id: 1,
    text: "中心思想",
    x: 100,
    y: 375,
    collapsed: false,
    children: [
      { id: 2, text: "分支1", collapsed: false, children: [] },
      { id: 3, text: "分支2", collapsed: false, children: [] },
    ]
  };

  function measureNode(node) {
    const words = wrapText(node.text, MIN_NODE_WIDTH);
    node.lines = words;
    node.width = Math.max(...words.map(line => ctx.measureText(line).width)) + PADDING * 2;
    node.height = words.length * LINE_HEIGHT + PADDING * 2;
  }

  function wrapText(text, maxWidth) {
    const words = text.split('');
    const lines = [];
    let line = '';
    for (let i = 0; i < words.length; i++) {
      let testLine = line + words[i];
      let metrics = ctx.measureText(testLine);
      if (metrics.width > maxWidth && line !== '') {
        lines.push(line);
        line = words[i];
      } else {
        line = testLine;
      }
    }
    lines.push(line);
    return lines;
  }

  function findParent(node, target) {
    if (!node.children) return null;
    for (let child of node.children) {
      if (child === target) return node;
      const res = findParent(child, target);
      if (res) return res;
    }
    return null;
  }

  function layoutTree(node, depth = 0, yOffset = 0) {
    measureNode(node);
    // 保存节点的深度信息
    node.depth = depth;
    // 计算当前节点和其父节点的最大宽度
    const parentNode = depth > 0 ? findParent(rootNode, node) : null;
    const maxWidth = Math.max(node.width, parentNode ? parentNode.width : 0);
    // 动态计算水平间距，确保至少有 MIN_HORIZONTAL_GAP 的距离
    const dynamicGap = Math.max(MIN_HORIZONTAL_GAP, maxWidth * HORIZONTAL_GAP_RATIO);
    node.x = offsetX + 100 + depth * dynamicGap;

    // 如果节点被收起，直接返回当前节点的位置
    if (node.collapsed) {
        node.y = offsetY + yOffset;
        return yOffset + node.height + VERTICAL_GAP;
    }

    // 如果没有子节点，直接返回当前节点的位置
    if (!node.children || node.children.length === 0) {
        node.y = offsetY + yOffset;
        return yOffset + node.height + VERTICAL_GAP;
    }

    let currentY = yOffset;
    for (let child of node.children) {
        currentY = layoutTree(child, depth + 1, currentY);
    }

    const topY = node.children[0].y;
    const bottomY = node.children[node.children.length - 1].y + node.children[node.children.length - 1].height;
    node.y = (topY + bottomY - node.height) / 2;
    return currentY;
  }

  function drawNode(node) {
    // 根据节点深度设置不同的样式
    const depth = node.depth || 0;
    
    // 为不同深度设置不同的阴影效果
    const shadowIntensity = Math.max(0.1, 0.3 - depth * 0.05);
    ctx.shadowColor = `rgba(0, 0, 0, ${shadowIntensity})`;
    ctx.shadowBlur = Math.max(4, 10 - depth * 1.5);
    ctx.shadowOffsetX = Math.max(1, 3 - depth * 0.5);
    ctx.shadowOffsetY = Math.max(1, 3 - depth * 0.5);

    // 根据深度创建不同的渐变背景
    const gradient = ctx.createLinearGradient(node.x, node.y, node.x, node.y + node.height);
    
    // 根据深度选择不同的颜色
    let colors;
    switch(depth) {
      case 0: // 根节点
        colors = ['#ffffff', '#e6f7ff'];
        break;
      case 1: // 一级节点
        colors = ['#ffffff', '#fff1f0'];
        break;
      case 2: // 二级节点
        colors = ['#ffffff', '#f6ffed'];
        break;
      case 3: // 三级节点
        colors = ['#ffffff', '#fff7e6'];
        break;
      default: // 更深层级的节点
        colors = ['#ffffff', '#f9f0ff'];
    }
    
    gradient.addColorStop(0, colors[0]);
    gradient.addColorStop(1, colors[1]);
    
    ctx.fillStyle = gradient;
    
    // 根据深度设置不同的边框颜色
    const borderColors = ['#4a90e2', '#f5222d', '#52c41a', '#fa8c16', '#722ed1'];
    ctx.strokeStyle = borderColors[Math.min(depth, borderColors.length - 1)];
    
    // 根据深度调整边框宽度
    ctx.lineWidth = Math.max(1, 3 - depth * 0.5);
    
    ctx.beginPath();
    // 根据深度调整圆角
    const cornerRadius = Math.max(5, 15 - depth * 2);
    ctx.roundRect(node.x, node.y, node.width, node.height, cornerRadius);
    ctx.fill();
    ctx.stroke();

    // 重置阴影，避免影响文字
    ctx.shadowColor = 'transparent';
    ctx.shadowBlur = 0;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = 0;

    // 绘制文字
    // 根据深度设置不同的文字颜色
    const textColors = ['#2c3e50', '#cf1322', '#389e0d', '#d46b08', '#531dab'];
    ctx.fillStyle = textColors[Math.min(depth, textColors.length - 1)];
    
    // 根据深度调整字体大小
    const fontSize = Math.max(14, 18 - depth);
    ctx.font = `${fontSize}px "Helvetica Neue", Arial, sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';

    node.lines.forEach((line, i) => {
      ctx.fillText(line, node.x + node.width / 2, node.y + PADDING + LINE_HEIGHT * i + LINE_HEIGHT / 2);
    });

    // 只有当节点有子节点时才绘制展开/收起按钮
    if (node.children && node.children.length > 0) {
      const buttonX = node.x + node.width + 10;
      const buttonY = node.y + node.height / 2;
      const buttonRadius = 8;
      
      ctx.beginPath();
      ctx.arc(buttonX, buttonY, buttonRadius, 0, Math.PI * 2);
      
      // 根据深度设置不同的按钮颜色
      const buttonColors = ['#4CAF50', '#f5222d', '#52c41a', '#fa8c16', '#722ed1'];
      ctx.fillStyle = buttonColors[Math.min(depth, buttonColors.length - 1)];
      ctx.fill();
      
      // 绘制加号或减号
      ctx.beginPath();
      ctx.strokeStyle = 'white';
      ctx.lineWidth = 2;
      ctx.moveTo(buttonX - 4, buttonY);
      ctx.lineTo(buttonX + 4, buttonY);
      if (node.collapsed) {
          ctx.moveTo(buttonX, buttonY - 4);
          ctx.lineTo(buttonX, buttonY + 4);
      }
      ctx.stroke();
    }
  }

  function drawLines(node) {
    if (!node.children || node.children.length === 0 || node.collapsed) return;

    const parentRight = node.x + node.width;
    const midX = parentRight + 30;
    const topY = node.children[0].y + node.children[0].height / 2;
    const bottomY = node.children[node.children.length - 1].y + node.children[node.children.length - 1].height / 2;
    const midY = (topY + bottomY) / 2;

    // 获取当前节点深度
    const depth = node.depth || 0;
    
    // 根据深度设置不同的线条样式
    const lineColors = ['#4a90e2', '#f5222d', '#52c41a', '#fa8c16', '#722ed1'];
    ctx.strokeStyle = lineColors[Math.min(depth, lineColors.length - 1)];
    
    // 根据深度调整线条宽度
    ctx.lineWidth = Math.max(1, 3 - depth * 0.5);
    ctx.lineCap = 'round';
    ctx.lineJoin = 'round';

    // 绘制从父节点到中心线的连接
    ctx.beginPath();
    ctx.moveTo(parentRight, node.y + node.height / 2);
    ctx.bezierCurveTo(
      parentRight + 15, node.y + node.height / 2,
      midX - 15, midY,
      midX, midY
    );
    ctx.stroke();

    // 绘制垂直中心线
    ctx.beginPath();
    ctx.moveTo(midX, topY);
    ctx.lineTo(midX, bottomY);
    ctx.stroke();

    // 绘制到每个子节点的连接
    node.children.forEach(child => {
      const childLeft = child.x;
      const childCenterY = child.y + child.height / 2;

      ctx.beginPath();
      ctx.moveTo(midX, childCenterY);
      ctx.bezierCurveTo(
        midX - 15, childCenterY,
        childLeft + 15, childCenterY,
        childLeft, childCenterY
      );
      ctx.stroke();

      drawLines(child);
    });
  }

  function drawMindMap() {
    ctx.setTransform(1, 0, 0, 1, 0, 0); // 重置变换
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    // 先计算布局
    layoutTree(rootNode);
    
    // 应用变换
    ctx.translate(offsetX, offsetY);
    ctx.scale(scale, scale);
    
    // 绘制内容
    drawLines(rootNode);
    drawNode(rootNode);
    function drawAllNodes(node) {
      if (!node.collapsed) {
        node.children.forEach(child => {
          drawNode(child);
          drawAllNodes(child);
        });
      }
    }
    drawAllNodes(rootNode);
  }


  function findNodeAt(x, y, node) {
    // 将屏幕坐标转换为画布坐标
    const canvasX = (x - offsetX) / scale;
    const canvasY = (y - offsetY) / scale;
    
    if (canvasX >= node.x && canvasX <= node.x + node.width && 
        canvasY >= node.y && canvasY <= node.y + node.height) {
      return node;
    }
    for (let child of node.children) {
      const result = findNodeAt(x, y, child);
      if (result) return result;
    }
    return null;
  }

  canvas.addEventListener('mousedown', (e) => {
    if (e.button === 0) {
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      const node = findNodeAt(x, y, rootNode);

      if (node && node.children && node.children.length > 0) {
        // 计算按钮在画布上的实际位置（考虑缩放和偏移）
        const buttonX = (node.x + node.width + 15) / scale;
        const buttonY = (node.y + node.height / 2) / scale;
        // 将鼠标坐标转换为画布坐标系
        const canvasX = (x - offsetX) / scale;
        const canvasY = (y - offsetY) / scale;
        const buttonRadius = 8;
        const dx = canvasX - buttonX;
        const dy = canvasY - buttonY;

        if (dx * dx + dy * dy <= buttonRadius * buttonRadius) {
          node.collapsed = !node.collapsed;
          drawMindMap();
          return;
        }
      }

      isDraggingCanvas = true;
      canvas.style.cursor = 'grabbing';
      lastMouseX = e.clientX;
      lastMouseY = e.clientY;
    }
  });

  let scale = 1;

  canvas.addEventListener('wheel', (e) => {
    e.preventDefault();
    const delta = e.deltaY > 0 ? -0.1 : 0.1;
    scale = Math.min(Math.max(0.2, scale + delta), 3);
    drawMindMap();
  });


  canvas.addEventListener('mousemove', (e) => {
    if (isDraggingCanvas) {
      const dx = e.clientX - lastMouseX;
      const dy = e.clientY - lastMouseY;
      offsetX += dx;
      offsetY += dy;
      lastMouseX = e.clientX;
      lastMouseY = e.clientY;
      drawMindMap();
    }
  });

  canvas.addEventListener('mouseup', () => {
    isDraggingCanvas = false;
    canvas.style.cursor = 'grab';
  });

  canvas.addEventListener('mouseleave', () => {
    isDraggingCanvas = false;
    canvas.style.cursor = 'grab';
  });

  canvas.addEventListener('contextmenu', (e) => {
    e.preventDefault();
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const node = findNodeAt(x, y, rootNode);
    if (node) {
      currentNode = node;
      contextMenu.style.left = `${e.clientX}px`;
      contextMenu.style.top = `${e.clientY}px`;
      contextMenu.style.display = 'block';
    } else {
      contextMenu.style.display = 'none';
    }
  });

  document.addEventListener('click', () => {
    contextMenu.style.display = 'none';
  });

  addChildBtn.addEventListener('click', () => {
    if (currentNode) {
      currentNode.children.push({ id: ++nodeIdCounter, text: '新子节点', children: [] });
      drawMindMap();
    }
  });

  addSiblingBtn.addEventListener('click', () => {
    const parent = findParent(rootNode, currentNode);
    if (parent) {
      parent.children.push({ id: ++nodeIdCounter, text: '新兄弟节点', children: [] });
      drawMindMap();
    }
  });

  deleteBtn.addEventListener('click', () => {
    if (!currentNode || currentNode === rootNode) return;
    function deleteNode(parent, target) {
      if (!parent.children) return false;
      const index = parent.children.findIndex(child => child === target);
      if (index !== -1) {
        parent.children.splice(index, 1);
        return true;
      }
      for (let child of parent.children) {
        if (deleteNode(child, target)) return true;
      }
      return false;
    }
    deleteNode(rootNode, currentNode);
    drawMindMap();
  });

  canvas.addEventListener('dblclick', (e) => {
    const rect = canvas.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const node = findNodeAt(x, y, rootNode);
    if (node) {
      editor.value = node.text;
      const screenX = node.x * scale + offsetX;
      const screenY = node.y * scale + offsetY;
      editor.style.left = `${screenX}px`;
      editor.style.top = `${screenY}px`;
      editor.style.width = `${node.width}px`;
      editor.style.height = `${node.height}px`;
      editor.style.transform = `scale(${scale})`;
      editor.style.transformOrigin = 'top left';
      editor.style.display = 'block';
      editor.focus();
      editor.setSelectionRange(0, editor.value.length);
      editor.dataset.nodeId = node.id;
    }
  });

  editor.addEventListener('blur', () => {
    const nodeId = parseInt(editor.dataset.nodeId);
    function updateText(node) {
      if (node.id === nodeId) {
        node.text = editor.value;
      } else {
        node.children.forEach(updateText);
      }
    }
    updateText(rootNode);
    editor.style.display = 'none';
    drawMindMap();
  });

  CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
    this.beginPath();
    this.moveTo(x + r, y);
    this.arcTo(x + w, y, x + w, y + h, r);
    this.arcTo(x + w, y + h, x, y + h, r);
    this.arcTo(x, y + h, x, y, r);
    this.arcTo(x, y, x + w, y, r);
    this.closePath();
  };

  drawMindMap();
</script>
</body>
</html>
